import { Event, EventEmitter, Uri } from "vscode";
import { createDeferred } from "../../../../common/utils/async";
import { traceError } from "../../../../logging";
import { EQEnvInfo, EQEnvKind } from "../../info";
import { EQLocatorQuery, IDiscoveryAPI, IEQEnvsIterator, IResolvingLocator } from "../../locator";
import { getQueryFilter } from "../../locatorUtils";
import { EQEnvCollectionChangedEvent, EQEnvsWatcher } from "../../watcher";
import { IEnvsCollectionCache } from './envsCollectionCache';

export class EnvsCollectionService extends EQEnvsWatcher<EQEnvCollectionChangedEvent> implements IDiscoveryAPI {
    constructor(private readonly cache: IEnvsCollectionCache, private readonly locator: IResolvingLocator) {
        super();
        this.locator.onChanged((event) => {
            const query = undefined; // We can also form a query based on the event, but skip that for simplicity.
            let scheduledRefresh = this.scheduledRefreshes.get(query);
            // If there is no refresh scheduled for the query, start a new one.
            if (!scheduledRefresh) {
                scheduledRefresh = this.scheduleNewRefresh(query);
            }
            scheduledRefresh.then(() => {
                // Once refresh of cache is complete, notify changes.
                this.fire(event);
            });
        });
        this.cache.onChanged((e) => {
            this.fire(e);
        });
    }
    public get onRefreshStart(): Event<void> {
        return this.refreshStarted.event;
    }

    public getEnvs(query?: EQLocatorQuery): EQEnvInfo[] {
        const cachedEnvs = this.cache.getAllEnvs();
        if (cachedEnvs.length === 0 && this.refreshPromises.size === 0) {
            // We expect a refresh to already be triggered when activating discovery component.
            traceError('No EQ is installed or a refresh has not already been triggered');
            this.triggerRefresh().ignoreErrors();
        }
        return query ? cachedEnvs.filter(getQueryFilter(query)) : cachedEnvs;
    }
    triggerRefresh(query?: { kinds?: EQEnvKind[] | undefined; } & { searchLocations?: { roots: Uri[]; doNotIncludeNonRooted?: boolean | undefined; } | undefined; } & { clearCache?: boolean | undefined; }, trigger?: "auto" | "ui"): Promise<void> {
        let refreshPromise = this.getRefreshPromiseForQuery(query);
        if (!refreshPromise) {
            refreshPromise = this.startRefresh(query);
        }
        return refreshPromise;
    }

    private readonly refreshStarted = new EventEmitter<void>();

    private scheduledRefreshes = new Map<EQLocatorQuery | undefined, Promise<void>>();

    private refreshPromises = new Map<EQLocatorQuery | undefined, Promise<void>>();

    private getRefreshPromiseForQuery(query?: EQLocatorQuery) {
        // Even if no refresh is running for this exact query, there might be other
        // refreshes running for a superset of this query. For eg. the `undefined` query
        // is a superset for every other query, only consider that for simplicity.
        return this.refreshPromises.get(query) ?? this.refreshPromises.get(undefined);
    }

    private async scheduleNewRefresh(query?: EQLocatorQuery): Promise<void> {
        const refreshPromise = this.getRefreshPromiseForQuery(query);
        let nextRefreshPromise: Promise<void>;
        if (!refreshPromise) {
            nextRefreshPromise = this.startRefresh(query);
        } else {
            nextRefreshPromise = refreshPromise.then(() => {
                // No more scheduled refreshes for this query as we're about to start the scheduled one.
                this.scheduledRefreshes.delete(query);
                this.startRefresh(query);
            });
            this.scheduledRefreshes.set(query, nextRefreshPromise);
        }
        return nextRefreshPromise;
    }

    private startRefresh(query: (EQLocatorQuery & { clearCache?: boolean }) | undefined): Promise<void> {
        const deferred = createDeferred<void>();

        if (query?.clearCache) {
            this.cache.clearCache();
        }
        // Ensure we set this before we trigger the promise to accurately track when a refresh has started.
        this.refreshPromises.set(query, deferred.promise);
        this.refreshStarted.fire();
        const iterator = this.locator.iterEnvs(query);
        const promise = this.addEnvsToCacheFromIterator(iterator);
        return promise
            .then(async () => {
                // Ensure we delete this before we resolve the promise to accurately track when a refresh finishes.
                this.refreshPromises.delete(query);
                deferred.resolve();
            })
            .catch((ex) => deferred.reject(ex));
    }

    private async addEnvsToCacheFromIterator(iterator: IEQEnvsIterator) {
        const seen: EQEnvInfo[] = [];
        const state = {
            done: false,
            pending: 0,
        };
        const updatesDone = createDeferred<void>();

        if (iterator.onUpdated !== undefined) {
            const listener = iterator.onUpdated(async (event) => {
                if (event === null) {
                    state.done = true;
                    listener.dispose();
                } else {
                    state.pending += 1;
                    this.cache.updateEnv(seen[event.index], event.update);
                    if (event.update) {
                        seen[event.index] = event.update;
                    }
                    state.pending -= 1;
                }
                if (state.done && state.pending === 0) {
                    updatesDone.resolve();
                }
            });
        } else {
            updatesDone.resolve();
        }

        for await (const env of iterator) {
            seen.push(env);
            this.cache.addEnv(env);
        }
        await updatesDone.promise;
        await this.cache.validateCache();
        this.cache.flush().ignoreErrors();
    }
}